import glob, threading, urllib.request, tempfile, uuid, os, requests, shutil, time, datetime
from tools import checker
from tools.OperationError import OperationError
from tools.auto_property import prop
from tools.httpdoer import Request
import tools.convert as convert
from tools.convert import translate as t
from webapp.config import Config


class DownloadTask(object):
    id = prop("id", bind_field="_id", readonly=True)
    url = prop("url", bind_field="_url", readonly=True)
    local = prop("local", bind_field="_local", readonly=True)
    md5 = prop("md5", bind_field="_md5", readonly=True)
    etag = prop("etag", bind_field="_etag", readonly=True)
    total = prop("total", bind_field="_total", check_type=int)
    data = prop("data", bind_field="_data", readonly=True)
    current = prop("current", bind_field="_current", check_type=int, default=0)
    percent = prop("percent", bind_field="_percent", readonly=True, default=0)
    done = prop("done", bind_field="_done", check_type=bool, default=False)
    valid = prop("valid", bind_field="_valid", check_type=bool, default=None)
    state = prop("state", bind_field="_state", check_values=(1, 2, 3, 4), doc="1下载中, 2暂停, 3完成, 4删除")
    create_at = prop("create_at", bind_field="_create_at", readonly=True)

    def __init__(self, *, local, md5, url, etag, total, task_id=None, create_at=None, data=None):
        self._data = data
        self._id = task_id or uuid.uuid1().hex
        self._url = url
        self._local = local
        self._md5 = md5
        self._etag = etag
        self.total = total
        self._state = OneTimeDownloadState(2)
        self._create_at = create_at or datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self._exception = None

    def _set_done(self, value):
        if value:
            if self.md5:
                file_md5 = convert.file_to_md5(self.local)
                self.valid = convert.verify(file_md5, self.md5)
            self._percent = 100
            self.state = 3
        self._done = value

    def _set_total(self, value):
        self._total = value
        if value <= 0:
            self._percent = 0
        else:
            self._percent = min((self.current / self.total) * 100, 99.99)

    def _set_current(self, value):
        self._current = value
        if value < 0:
            self._percent = -1
        else:
            self._percent = min((self.current / self.total) * 100, 99.99)

    def _set_state(self, value):
        self._state = self._state.change(value)

    def pause(self):
        self.state = 2

    def resume(self, processor=None):
        undone_file = UndoneFile.get(self.id)
        if self.state == 2 and undone_file:
            undone_file.down_async(processor)
            return True
        return False

    def get_exception(self):
        return self._exception

    def set_exception(self, exception):
        self._exception = exception
        exception and self.pause()


class OneTimeDownloadState(object):

    def __init__(self, value):
        self._value = value
        self._fixed = None

    def change(self, value):
        checker.assertRange("value", value, (1, 2, 3, 4))
        if self._value in (3, 4):  # 状态是3或者4时 将不能再更新状态
            return self
        self._fixed = self._fixed or value
        return OneTimeDownloadState(value)

    def __eq__(self, other):
        if isinstance(other, OneTimeDownloadState):
            other = other._fixed or other._value
        return (self._fixed or self._value) == other

    def __str__(self):
        value = self._fixed or self._value
        if value == 1:
            return "1: 下载中"
        if value == 2:
            return "2: 暂停"
        if value == 3:
            return "3: 完成"
        if value == 4:
            return "4: 删除"
        return "未知"

    def __repr__(self):
        return str(self._fixed or self._value)


class UndoneFile(object):
    __cache__ = {}

    @classmethod
    def get(cls, task_id):
        return cls.__cache__.get(task_id)

    @classmethod
    def add(cls, undone_file):
        if undone_file:
            return cls.__cache__.setdefault(undone_file.task.id, undone_file) is undone_file
        return False

    @classmethod
    def list(cls):
        return tuple(cls.__cache__.values())

    @classmethod
    def reload(cls):
        for info in glob.glob(
                os.path.join(tempfile.gettempdir(), Config.DOWNLOAD_UNDONE_DIR or "download_undone", "*\\info.json")):
            with open(info, 'r') as f:
                text = f.read()
            json = convert.to_json_obj(text)
            task = DownloadTask(task_id=json.id,
                                create_at=json.create_at,
                                url=json.url,
                                local=json.local,
                                md5=json.md5,
                                etag=json.etag,
                                total=json.total,
                                data=json.data)
            task.pause()
            task.current = json.current
            cls.add(UndoneFile(task))

    def __init__(self, task):
        checker.assertNotEmpty("task", task)
        self.task = task
        self.task_id = task.id
        self.dir = os.path.join(tempfile.gettempdir(), Config.DOWNLOAD_UNDONE_DIR or "download_undone", task.id)
        self.info = os.path.join(self.dir, "info.json")
        self.content = os.path.join(self.dir, "content.part")
        self.chunk_size = 1000 * 1000
        self.is_repeat = not UndoneFile.add(self)

    def check(self):
        try:
            self.is_repeat or os.makedirs(self.dir)
        except FileExistsError:
            self.is_repeat = True

    def save_info(self):
        if self.is_repeat:
            return
        json = convert.to_json_string(self.task)
        with open(self.info, 'w') as f:
            f.write(json)

    def load_info(self):
        if self.is_repeat:
            return
        if not os.path.exists(self.info):
            return
        with open(self.info, 'r') as f:
            json = f.read()
        self.task.current = min(os.path.getsize(self.content),  # 文件实际大小
                                convert.to_json_obj(json).current,  # info文件中记录的大小
                                self.task.current)  # 一个包的大小
        self.task.current = max(self.task.current - self.chunk_size, 0)  # 回滚一个包

    def open_content(self):
        if self.is_repeat:
            return
        if os.path.exists(self.content):
            return open(self.content, "rb+")
        return open(self.content, "xb")

    def down(self, processor):
        if self.is_repeat:
            return
        if not callable(processor):
            processor = None
        try:
            task = self.task
            task.set_exception(None)
            state = task.state  # 这里的状态是一个信号量, 必须要暂存在本地变量后使用
            chunk_size = self.chunk_size
            if state != 1:
                return
            self.load_info()
            headers = {'Range': 'bytes=%d-' % task.current}
            response = requests.get(task.url, stream=True, headers=headers)
            if response.headers["ETag"] != task.etag:
                raise OperationError(-3, t("文件下载失败") + "3")
            processor and processor(task)
            with self.open_content() as appender:
                appender.seek(task.current, os.SEEK_SET)
                appender.flush()
                for chunk in response.iter_content(chunk_size=chunk_size):
                    if chunk:
                        appender.write(chunk)
                        appender.flush()
                        task.current = min(task.current + chunk_size, task.total)
                        self.save_info()
                        processor and processor(task)
                    if state != 1:
                        if state == 4:
                            appender.close()
                            self.delete()
                        return
            self.done()
            processor and processor(task)
        except BaseException as e:
            task.set_exception(e)
            processor and processor(task)

    def done(self):
        if self.is_repeat:
            return
        task = self.task
        if os.path.getsize(self.content) != task.total:
            raise OperationError(-1, t("文件下载失败") + "1")
        if task.done:
            raise OperationError(-2, t("文件下载失败") + "2")
        path = os.path.dirname(task.local)
        if not os.path.exists(path):
            os.makedirs(path)  # 创建路径
        shutil.move(self.content, task.local)  # 移动文件
        task.done = True
        self.delete()

    def down_async(self, processor):
        if self.is_repeat or self.task.state != 2:
            return
        self.task.state = 1
        threading.Thread(target=self.down, args=(processor,)).start()

    def delete(self):
        try:
            shutil.rmtree(self.dir)  # 递归删除文件夹
            UndoneFile.__cache__.pop(self.task_id)
        except Exception as e:
            print(e)
        self.task.state = 4


def download(url, local=None, processor=None, *, md5=None, task_id=None, data=None):
    """
    下载文件
    :param task_id:
    :return:
    :param url: 下载路径
    :param local: 本地文件路径
    :param processor: DownloadTask { total: 需下载字节, current: 已下载字节, local: 本地文件路径, percent: 下载百分比 }
    :param data: 额外的数据
    :param md5: 文件摘要
    :return: 返回一个对象 DownloadTask
    """
    if local is None:
        local = tempfile.mkdtemp()

    req = Request("head", url)
    res = req.send()
    content_md5 = res.headers.get("Content-MD5")

    task = DownloadTask(
        task_id=task_id,
        url=url,
        local=local,
        md5=md5 if md5 or not content_md5 else convert.decode_base64(content_md5).bytes.hex(),
        etag=res.headers.get("ETag") or "",
        total=int(res.headers.get("Content-Length")),
        data=data)

    if not callable(processor):
        processor = None
    # 服务支持断点续传 res.headers.get("Accept-Ranges") == "bytes"
    if (Config.DOWNLOAD_MIN_FILE_SIZE or 1000000) > task.total and res.headers.get("Accept-Ranges") == "bytes":
        # 小文件直接下载, 小文件下载无法暂停和恢复

        def reporthook(arg1, arg2, arg3):
            """
            下载回调
            :param arg1: 已经下载的数据块
            :param arg2: 数据块的大小
            :param arg3: 远程文件的大小
            """
            task.total = arg3
            task.current = min(arg3, arg1 * arg2)
            processor and processor(task)

        def direct():
            try:
                import socket
                socket.setdefaulttimeout(15.0)
                urllib.request.urlretrieve(url, local, reporthook)
                task.done = True
                processor and processor(task)
            except BaseException as e:
                task.set_exception(e)
                processor and processor(task)

        threading.Thread(target=direct).start()
    else:
        undone_file = UndoneFile(task)
        undone_file.check()
        if undone_file.is_repeat:
            raise OperationError("RepeatDownload")

        undone_file.down_async(processor)

    return task


def undone_list():
    """
    获取未完成的文件下载列表
    :return: UndoneFile[]
    """
    return UndoneFile.list()


def get_undone(task_id):
    """
    根据id获取未完成的任务
    :param task_id:
    :return: UndoneFile
    """
    return UndoneFile.get(task_id)


def pause(task_id):
    """
    暂停指定id的任务
    :param task_id:
    """
    u = UndoneFile.get(task_id)
    if u is None or u.task.state == 3:
        return False
    if u.task.state == 2:
        return True
    u.task.pause()
    return True


def resume(task_id, processor=None):
    """
    继续已暂停的任务
    :param task_id:
    :param processor:
    """
    u = UndoneFile.get(task_id)
    if u is None or u.task.state != 2:
        return False
    return u.task.resume(processor)


def delete(task_id):
    """
    删除下载任务
    :param task_id:
    :return:
    """
    u = UndoneFile.get(task_id)
    if u is None:
        return False
    u.delete()
    return True


def reload_undone_list():
    UndoneFile.reload()


if __name__ == "__main__":
    def test0():
        reload_undone_list()
        l = undone_list()

        if len(l) > 0:
            print("测试 断点续传")
            task = l[0].task
            resume(task.id, lambda p: print('当前进度: %.2f%%' % p.percent))

            while not task.done:
                time.sleep(1)
            print("下载完成")
            assert task.valid
            print("验证通过")
        print("断点续传 测试完成")


    test0()


    def test1():
        print("测试 OneTimeDownloadState")
        x = DownloadTask(url="", local="", md5="", etag="", total=0)
        s = x.state
        assert s == x.state
        x.state = 1
        assert s == 1
        x.state = 3
        assert x.state == 3
        assert s == 1
        s.change(3)
        assert s == 1
        print("OneTimeDownloadState 测试完成")


    test1()


    def test2():
        print("\n\n\n\n\n小文件直接下载")
        a = 'http://fcweb.oss-cn-hangzhou.aliyuncs.com/TEST/999/4738a024-a277-421f-bf24-2ff266b5b252'
        b = 'd:\\1.txt'
        task = download(a, b, lambda p: print('当前进度: %.2f%%' % p.percent))
        print(convert.to_json_string(task))
        print("下载到: %s" % task.local)
        import time

        while not task.done:
            time.sleep(1)
        print("下载完成")
        assert task.valid
        print("验证通过")


    test2()


    def test3():
        import time
        print("\n\n\n\n\n大文件断点续传")
        a = 'http://fc-pos.oss-cn-hangzhou.aliyuncs.com/10M'
        b = 'd:\\data'
        task = download(a, b, lambda p: print('当前进度: %.2f%%' % p.percent), md5="ad6718728870578b4cb37bf44c320f34",
                        data={"id": 1})
        print("下载到: %s" % task.local)
        time.sleep(3)
        print("暂停下载")
        task.pause()
        for i in range(3):
            time.sleep(1)
            print(str(3 - i))
        print("恢复下载")
        task.resume(lambda p: print('当前进度: %.2f%%' % p.percent))

        while not task.done:
            time.sleep(1)
        print("下载完成")
        assert task.valid
        print("验证通过")


    test3()


    def test4():
        import time
        print("\n\n\n\n\n测试删除文件")
        a = 'http://fc-pos.oss-cn-hangzhou.aliyuncs.com/10M'
        b = 'd:\\data'
        task = download(a, b, lambda p: print('当前进度: %.2f%%' % p.percent), md5="ad6718728870578b4cb37bf44c320f34")
        print("下载到: %s" % task.local)
        for i in range(3):
            time.sleep(1)
            print(str(3 - i))
        print("删除")
        u = get_undone(task.id)
        delete(task.id)
        print("删除完成")
        while os.path.exists(u.dir):
            time.sleep(1)
        print("验证通过")


    test4()
